﻿using System;
using System.Collections.Generic;
using System.Linq;
using Netfraction.Common;
using Netfraction.Common.DataStorage;
using NetfractionHub.Network.Protocol.BASE.Factory;
using NetfractionHub.Network.Protocol;
using System.Threading.Tasks;

namespace NetfractionHub.Network
{
	/// <summary>
	/// The ConnectionManager is the heart of the hub
	/// </summary>
	public class ConnectionManager
	{
		/// <summary>
		/// Keeps track of all Session ID's that are not in use.
		/// </summary>
		private static Queue<string> _freeSIDs;

		/// <summary>
		/// The currently connected nodes in the hub, including human clients and bots.
		/// </summary>
		private static HotSwapList<RemoteMachine.Node> _connectedNodes;
		private static HotSwapList<RemoteMachine.Node> _pendingNodes;
		/// <summary>
		/// The bots currently running on the hub.
		/// </summary>
		private static SynchronizedCollection<LocalMachine.BotNode> _bots;
		private static LocalMachine.Hub _hub;

		private static bool _hubHasOwner;
		public static bool HubHasOwner
		{
			get { return _hubHasOwner; }
			set { _hubHasOwner = value; }
		}

		/// <summary>
		/// Stores all the bans in place for the hub.
		/// </summary>
		private static SqlDataStore _banStore = null;

		public static event ClientConnected OnClientConnected;

		public static SqlDataStore GetBanStore()
		{
			return ConnectionManager._banStore;
		}

		private static bool CheckBanStoreExists()
		{
			// do initialisation incl. creating tables if needed
			return true;
		}

		/// <summary>
		/// A Node specifically for the hub, including it's Identity details and supported ProtocolFeatures.
		/// </summary>
		public static LocalMachine.Hub Hub
		{
			get { return _hub; }
			set { _hub = value; }
		}

		public static int ConnectedNodeCount
		{
			get
			{
				return _connectedNodes.Count;
			}
		}

		public static long TotalShareSize
		{
			get
			{
				if (_connectedNodes.Count > 0)
				{
					return _connectedNodes.Sum(p => p.Identity.ShareSize);
				}
				else
				{
					return 0;
				}
			}
		}

		public static long TotalFilesShared
		{
			get
			{
				if (_connectedNodes.Count > 0)
				{
					return _connectedNodes.Sum(p => p.Identity.SharedFilesCount);
				}
				else
				{
					return 0;
				}
			}
		}

		private ConnectionManager()
		{
			if (_instance == null)
				_instance = this;

			_hubHasOwner = false;

			InitialiseSIDs();

			_hub = LocalMachine.Hub.GetInstance();

			_connectedNodes = new HotSwapList<RemoteMachine.Node>();
			_pendingNodes = new HotSwapList<RemoteMachine.Node>();

			if (_bots != null)
			{
				lock (_bots.SyncRoot)
				{
					_bots = new SynchronizedCollection<LocalMachine.BotNode>();
				}
			}
			else
			{
				_bots = new SynchronizedCollection<LocalMachine.BotNode>();
			}

			RegisterBot(LocalMachine.SecurityBot.GetInstance());


			RegisterBot(AnnouncerBot.AnnouncerBot.GetInstance()); // should move to seperate project
			//RegisterBot(new LocalMachine.ChannelBot()); // this isn't doing anything useful yet.
		}

		#region Singleton Pattern
		// Use singleton pattern.
		private static ConnectionManager _instance;

		/// <summary>
		/// Obtain singleton instance of ConnectionManager
		/// </summary>
		/// <returns></returns>
		public static ConnectionManager GetInstance()
		{
			// Check instance
			if (ConnectionManager._instance == null)
			{
				// Initialise instance
				ConnectionManager._instance = new ConnectionManager();
			}

			// Return instance
			return ConnectionManager._instance;
		}
		#endregion Singleton Pattern

		/// <summary>
		/// When a socket connection has been established, add the node to the connected list.
		/// </summary>
		/// <param name="connectingSocket">The new socket connection.</param>
		public static void OnConnectClient(System.Net.Sockets.Socket connectingSocket)
		{
			if (connectingSocket == null)
				return;

			_pendingNodes.Add(new RemoteMachine.Client(connectingSocket));
		}

		/// <summary>
		/// When the connection process is complete, this method broadcasts the details of the new node to others.
		/// </summary>
		/// <param name="node">The newly connected node.</param>
		internal static void OnClientConnectSuccess(RemoteMachine.Node node)
		{
			if (node == null)
				return;

			_connectedNodes.Add(node);
			_pendingNodes.Remove(node);

			if (!_hubHasOwner)
			{
				Security.Identity genericOwnerIdentity = Security.Identity.CreateGenericIdentityForRole(Security.RoleManager.GetRoleById(1));
				genericOwnerIdentity.Name = node.Identity.Nickname;
				genericOwnerIdentity.ClientId = node.Identity.ClientId;

				Security.IdentityManager.PersistRole(genericOwnerIdentity);
				node.SecurityIdentity = genericOwnerIdentity;
				node.Identity.NodeType = node.Identity.NodeType | IdentityNodeTypes.HubOwner;

				_hubHasOwner = true;
			}

			if (node.Identity.ClientId != Settings.HubSettings.HubIdentity.UserInterfaceClientId)
				ConnectionManager.SendNodeInformationToAll(node);

			node.Connection.State = NetfractionHub.RemoteMachine.ConnectionState.Normal;

			if (OnClientConnected != null)
				OnClientConnected(node);

			ConnectionManager.SendGenericStatusMessageToNode(node, Settings.HubSettings.HubIdentity.MessageOfTheDay);
		}

		/// <summary>
		/// Create the list of available Session ID's for connecting nodes.
		/// </summary>
		private void InitialiseSIDs()
		{
			// The size of this queue only really needs to be MAX_USERS, not 2^16.
			// A Queue is a first-in, first-out (FIFO) collection implemented as a circular array.
			// Objects are inserted at one end and removed from the other.
			int limit = Settings.HubSettings.HubConnection.MaximumUsersCount;
			_freeSIDs = new Queue<string>(limit);
			for (int i = 0; i <= (limit - 1); i++)
				_freeSIDs.Enqueue(Base32.Encode(BitConverter.GetBytes(i)).Substring(0, 4));
		}

		/// <summary>
		/// Retrieves a Session ID for a connecting node.
		/// </summary>
		/// <param name="node">The node requiring the Session ID.</param>
		/// <returns>The assigned Session ID.</returns>
		public static string GenerateNewSessionId(RemoteMachine.Node node)
		{
			if (node == null)
				return string.Empty;

			string sidString = string.Empty;
			bool creationSuccessful = false;

			while (!creationSuccessful)
			{
				creationSuccessful = true;
				sidString = _freeSIDs.Dequeue();

				// by default, use ABCD for operator chat and DCBA for the security bot
				if (sidString == Settings.HubSettings.OperatorBot.SessionId || sidString == Settings.HubSettings.SecurityBot.SessionId)
				{
					creationSuccessful = false;
					continue;
				}
			}
			if (node is RemoteMachine.Client)
			{
				try
				{
					lock (_connectedNodes)
					{
						_pendingNodes[_pendingNodes.IndexOf(node as RemoteMachine.Client)].SessionId = sidString;
					}
				}
				catch (ArgumentOutOfRangeException aore)
				{
					Console.WriteLine("dropped connection: " + node.Identity.Nickname);
					System.Diagnostics.Debug.WriteLine("dropped connection: " + node.Identity.Nickname);
					System.Diagnostics.Debug.WriteLine(aore.Message);
				}
			}
			else if (node is LocalMachine.BotNode)
			{
				lock (_bots.SyncRoot)
				{
					_bots[_bots.IndexOf(node as LocalMachine.BotNode)].SessionId = sidString;
				}
			}
			return sidString;
		}

		/// <summary>
		/// Generates a Client Id by creating a hash of an input string.
		/// </summary>
		/// <param name="input">The string to hash. Must be unique to avoid clashing with other clients.</param>
		/// <returns>The generated Client Id.</returns>
		public static string GenerateClientId(RemoteMachine.Node node, string input)
		{
			if (string.IsNullOrEmpty(input) || node == null)
				return string.Empty;

			return GenerateClientId(node, System.Text.Encoding.UTF8.GetBytes(input));
		}

		public static string GenerateClientId(RemoteMachine.Node node, byte[] input)
		{
			if (input.Length == 0 || node == null)
				return string.Empty;

			return GenerateClientId(node.SessionHashAlgorithm, input);
		}

		public static string GenerateClientId(System.Security.Cryptography.HashAlgorithm hashAlgorithm, string input)
		{
			if (string.IsNullOrEmpty(input) || hashAlgorithm == null)
				return string.Empty;

			return GenerateClientId(hashAlgorithm, System.Text.Encoding.UTF8.GetBytes(input));
		}

		public static string GenerateClientId(System.Security.Cryptography.HashAlgorithm hashAlgorithm, byte[] input)
		{
			if (input.Length == 0 || hashAlgorithm == null)
				return string.Empty;

			hashAlgorithm.Initialize();
			return Base32.Encode(hashAlgorithm.ComputeHash(input)).Substring(0, 39);	// TODO: this won't necessarily work for another hash algorithm
		}

		/// <summary>
		/// Registers a bot with the hub.
		/// </summary>
		/// <param name="bot">The BotNode to register.</param>
		public static void RegisterBot(LocalMachine.BotNode bot)
		{
			if (bot == null)
				return;

			// Use a nested lock because we want these operations to be atomic.
			// IMPORTANT: GenerateNewSessionId cannot be called inside these locks, as it in turn places a lock 
			// on _connectedNodes and _bots, which would result in a deadlock.
			lock (_bots.SyncRoot)
			{
				_bots.Add(bot);
				_connectedNodes.Add(bot);
			}
			GenerateNewSessionId(bot);
			bot.OnStart();
		}

		public static bool CheckForBotCommand(string message, out List<LocalMachine.BotNode> handlerBots)
		{
			var handler = from p in _bots
						  where LocalMachine.BotNode.GetCommandPrefixes(p).Any(q => message.StartsWith(q, StringComparison.InvariantCultureIgnoreCase))
						  select p;

			if (handler.Count() > 0)
			{
				handlerBots = handler.ToList();
				return true;
			}

			handlerBots = new List<NetfractionHub.LocalMachine.BotNode>();
			return false;
		}

		/// <summary>
		/// Transmits a message to all BotNodes registered for security messages.
		/// </summary>
		/// <param name="sourceNode">The sending node.</param>
		/// <param name="message">The message to send.</param>
		public static void SendMessageToSecurityBots(RemoteMachine.Node sourceNode, string message)
		{
			if (sourceNode == null)
				return;

			// Need the permission check because an admin may revoke the permission, despite the bot being of type SecurityBot
			foreach (LocalMachine.BotNode bot in _bots)
				if (bot is LocalMachine.SecurityBot && (bot as LocalMachine.SecurityBot).HasSecurityBotPermission)
					Task.Factory.StartNew(() => bot.HandleCommandMessage(sourceNode, message));
		}

		public static void KickNode(RemoteMachine.Node sourceNode, RemoteMachine.Node kickedNode, string message, int durationSeconds)
		{
			// should I perform the security checks here? or leave it to the calling functions? perhaps a private kick function, 
			// with the public one performing security checks. BUT, given the range of checks required, it would probably have to 
			// return an enum with an error/success code.

			if (sourceNode == null || kickedNode == null)
				return;

			Dictionary<int, object> factoryParams = new Dictionary<int, object>();
			factoryParams.Add((int)Protocol.BASE.Factory.FactoryQUI.ParameterEnum.SessionID, kickedNode.SessionId);
			factoryParams.Add((int)Protocol.BASE.Factory.FactoryQUI.ParameterEnum.InitiatorSID, sourceNode.SessionId);

			if (!string.IsNullOrEmpty(message))
				factoryParams.Add((int)Protocol.BASE.Factory.FactoryQUI.ParameterEnum.Message, message);

			factoryParams.Add((int)Protocol.BASE.Factory.FactoryQUI.ParameterEnum.TimeToReconnect, durationSeconds);
			factoryParams.Add((int)Protocol.BASE.Factory.FactoryQUI.ParameterEnum.TerminateTransfers, string.Empty);

			Protocol.ProtocolMessageFactory factory = new Protocol.BASE.Factory.FactoryQUI();
			kickedNode.Connection.Send(factory.Create(Protocol.ProtocolMessage.MessageTypeEnum.Info, factoryParams).ToString());

			DisconnectNode(kickedNode, factoryParams);
		}

		public static void PrintPermissionsToDebug(Type permissionType)
		{
			Security.AccessPermissionBase perm = Security.PermissionBuilder.Create(permissionType);

			if (perm == null)
				return;

			System.Diagnostics.Debug.WriteLine("Begin Debug");
			foreach (RemoteMachine.Node node in _connectedNodes.AsReadOnly())
			{
				System.Diagnostics.Debug.WriteLine(String.Format("Node Nick: {0}, Permission: {1}, Has Permission: {2}", node.Identity.Nickname, perm.ToString(), Security.Permission.HasPermission(node.SecurityIdentity, permissionType)));
			}
			System.Diagnostics.Debug.WriteLine("End Debug");
		}

		/// <summary>
		/// Removes a node and broadcasts the departure to other nodes.
		/// </summary>
		/// <param name="disconnectingClient">The node to remove.</param>
		public static void DisconnectNode(NetfractionHub.RemoteMachine.Node disconnectingClient)
		{
			if (disconnectingClient == null)
				return;

			RemoveDisconnectedNode(disconnectingClient);

			if (!string.IsNullOrEmpty(disconnectingClient.SessionId))
			{
				string messageToNodes = "IQUI " + disconnectingClient.SessionId;

				try
				{
					foreach (RemoteMachine.Node cli in _connectedNodes.AsReadOnly())
					{
						cli.Connection.Send(messageToNodes, true);
					}
				}
				catch (InvalidOperationException ioex)
				{
					System.Diagnostics.Debug.WriteLine(string.Format("threading error: {0}", ioex.Message));
					//throw;
				}
			}
		}

		/// <summary>
		/// Removes a node and broadcasts the departure to other nodes.
		/// </summary>
		/// <param name="disconnectingClient">The node to remove.</param>
		private static void DisconnectNode(NetfractionHub.RemoteMachine.Node disconnectingClient, Dictionary<int, object> quitParameters)
		{
			if (disconnectingClient == null || quitParameters == null || quitParameters.ContainsKey((int)Protocol.BASE.Factory.FactoryQUI.ParameterEnum.SessionID) == false)
			{
				return;
			}

			RemoveDisconnectedNode(disconnectingClient);

			if (!string.IsNullOrEmpty(disconnectingClient.SessionId))
			{
				Protocol.ProtocolMessageFactory factory = new Protocol.BASE.Factory.FactoryQUI();
				string messageToNodes = factory.Create(Protocol.ProtocolMessage.MessageTypeEnum.Info, quitParameters).ToString();

				try
				{
					foreach (RemoteMachine.Node cli in _connectedNodes.AsReadOnly())
					{
						cli.Connection.Send(messageToNodes);
					}
				}
				catch (InvalidOperationException ioex)
				{
					System.Diagnostics.Debug.WriteLine(string.Format("threading error: {0}", ioex.Message));
					//throw;
				}
			}
		}

		/// <summary>
		/// Removes a disconnected node from the connected nodes list and frees it's Session ID.
		/// </summary>
		/// <param name="disconnectedClient">The Node which has been disconnected.</param>
		internal static void RemoveDisconnectedNode(NetfractionHub.RemoteMachine.Node disconnectedClient)
		{
			if (disconnectedClient == null)
				return;

			_connectedNodes.Remove(disconnectedClient);
			
			if (!string.IsNullOrEmpty(disconnectedClient.SessionId))
				if (!_freeSIDs.Contains(disconnectedClient.SessionId))
					_freeSIDs.Enqueue(disconnectedClient.SessionId);
			else
				System.Diagnostics.Debug.WriteLine(disconnectedClient.Identity.Nickname);
		}

		public static void SendNodeInformationToAll(RemoteMachine.Node connectingNode)
		{
			if (connectingNode == null)
				return;

			Protocol.ProtocolMessageFactory factory = new Protocol.BASE.Factory.FactoryINF();

			string messageToNodes = factory.Create(Protocol.ProtocolMessage.MessageTypeEnum.Broadcast, connectingNode.GetIdentityParameters()).ToString();
			foreach (RemoteMachine.Node cli in _connectedNodes.AsReadOnly())
			{
				if (cli.Connection.State == RemoteMachine.ConnectionState.Normal && cli is RemoteMachine.Client)
				{
					connectingNode.Connection.Send(cli.Identity.BroadcastInfoString, true);//factory.Create(Protocol.ProtocolMessage.MessageTypeEnum.Broadcast, cli.GetIdentityParameters()));

					cli.Connection.Send(messageToNodes, true);
				}
				else if (cli is LocalMachine.BotNode)
				{
					connectingNode.Connection.Send(factory.Create(Protocol.ProtocolMessage.MessageTypeEnum.Broadcast, cli.GetIdentityParameters()).ToString(), true);
				}
			}

			if (!(connectingNode is LocalMachine.BotNode))
				connectingNode.Connection.Send(factory.Create(Protocol.ProtocolMessage.MessageTypeEnum.Broadcast, connectingNode.GetIdentityParameters()).ToString());
		}

		public static void SendNodeInformationUpdateToAll(Dictionary<int, object> parameters)
		{
			if (parameters == null || parameters.Count == 0)
				return;

			Protocol.ProtocolMessageFactory factory = new Protocol.BASE.Factory.FactoryINF();
			string messageToNodes = factory.Create(Protocol.ProtocolMessage.MessageTypeEnum.Broadcast, parameters).ToString();
			foreach (RemoteMachine.Node cli in _connectedNodes.AsReadOnly())
			{
				if (cli.Connection.State == RemoteMachine.ConnectionState.Normal && cli is RemoteMachine.Client)
					cli.Connection.Send(messageToNodes, true);
			}
		}

		internal static void SendChatMessage(Protocol.ProtocolMessage protoMessage)
		{
			if (protoMessage == null)
			{
				return;
			}

			RemoteMachine.Node sourceClient;
			RemoteMachine.Node targetClient;

			string messageToNodes = protoMessage.ToString();

			switch (protoMessage.MessageType)
			{
				case NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Broadcast:
					foreach (RemoteMachine.Node node in _connectedNodes.AsReadOnly())
					{
						if (node.Connection.State == RemoteMachine.ConnectionState.Normal && node.Identity.AwayStatus != IdentityAwayStatus.ExtendedAway)
							node.Connection.Send(messageToNodes);
					}
					lock (_bots.SyncRoot)
					{
						foreach (LocalMachine.BotNode bot in _bots)
						{
							bot.HandleChatMessage(GetNodeBySessionId(protoMessage.SessionId), protoMessage);
						}
					}
					break;
				case NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Echo:
					sourceClient = GetNodeBySessionId(protoMessage.SessionId);
					targetClient = GetNodeBySessionId(protoMessage.TargetSessionId);
					if (sourceClient != null && targetClient != null)
					{
						if (sourceClient is LocalMachine.BotNode)
							(sourceClient as LocalMachine.BotNode).HandleChatMessage(sourceClient, protoMessage);
						else
							sourceClient.Connection.Send(messageToNodes);

						if (targetClient is LocalMachine.BotNode)
							(targetClient as LocalMachine.BotNode).HandleChatMessage(sourceClient, protoMessage);
						else
							targetClient.Connection.Send(messageToNodes, true);
					}
					break;
				case NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Direct:
					sourceClient = GetNodeBySessionId(protoMessage.SessionId);
					targetClient = GetNodeBySessionId(protoMessage.TargetSessionId);

					if (sourceClient != null && targetClient != null)
						targetClient.Connection.Send(messageToNodes, true);
					break;
				default:
					break;
			}
		}

		internal static void SendGetPasswordRequestToNode(RemoteMachine.Node targetNode)
		{
			if (targetNode == null)
				return;

			Random rand = new Random();
			targetNode.Identity.PasswordSaltBytes = new byte[24];
			rand.NextBytes(targetNode.Identity.PasswordSaltBytes);

			Protocol.ProtocolMessageFactory factory = new Protocol.BASE.Factory.FactoryGPA();
			Dictionary<int, object> factoryParams = new Dictionary<int, object>();
			factoryParams.Add((int)Protocol.BASE.Factory.FactoryGPA.ParameterEnum.Data, Base32.Encode(targetNode.Identity.PasswordSaltBytes));

			if (targetNode != null)
			{
				targetNode.Connection.Send(factory.Create(Protocol.ProtocolMessage.MessageTypeEnum.Info, factoryParams).ToString());
				targetNode.Connection.State = NetfractionHub.RemoteMachine.ConnectionState.Verify;
			}
		}

		internal static void SendPeerConnectRequest(Protocol.ProtocolMessage protoMessage)
		{
			if (protoMessage == null)
				return;

			if (protoMessage.MessageType == NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Direct)
			{
				RemoteMachine.Node targetClient = GetNodeBySessionId(protoMessage.TargetSessionId);
				if (targetClient != null)
					targetClient.Connection.Send(protoMessage.ToString());
			}
		}

		internal static void SendPeerReverseConnectRequest(Protocol.ProtocolMessage protoMessage)
		{
			if (protoMessage == null)
				return;

			if (protoMessage.MessageType == NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Direct)
			{
				RemoteMachine.Node targetClient = GetNodeBySessionId(protoMessage.TargetSessionId);
				if (targetClient != null)
				{
					targetClient.Connection.Send(protoMessage.ToString());
				}
			}
		}

		internal static void SendSearchRequestToAll(Protocol.ProtocolMessage protoMessage)
		{
			if (protoMessage == null)
				return;

			if (protoMessage.MessageType == NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Broadcast)
			{
				foreach (RemoteMachine.Node node in _connectedNodes.AsReadOnly())
				{
					if (node.Connection.State == RemoteMachine.ConnectionState.Normal)
						node.Connection.Send(protoMessage.ToString());
				}
			}
			else if (protoMessage.MessageType == NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Feature)
			{
				var inclFeats = GetIncludeFeaturesFromMessage(protoMessage);
				var exclFeats = GetExcludeFeaturesFromMessage(protoMessage);

				foreach (RemoteMachine.Node node in _connectedNodes.AsReadOnly())
				{
					if (node.Connection.State == RemoteMachine.ConnectionState.Normal)
						if (CheckFeaturesAgainstNode(node, inclFeats, exclFeats))
							node.Connection.Send(protoMessage.ToString());
				}
			}
		}

		/// <summary>
		/// Analyses the <typeparamref name="Protocol.ProtocolMessage"/> for include features e.g. +TCP4.
		/// </summary>
		/// <param name="protocolMessage">A <typeparamref name="Protocol.ProtocolMessage"/> to check for include features.</param>
		/// <returns>A string-collection of the include features in the <paramref name="protocolMessage"/>.</returns>
		internal static IEnumerable<string> GetIncludeFeaturesFromMessage(Protocol.ProtocolMessage protocolMessage)
		{
			return (from p in protocolMessage.Parameters
					where p.ParameterValue.StartsWith("+") && p.ParameterValue.Length > 1
					select p.ParameterValue.Substring(1));
		}

		/// <summary>
		/// Analyses the <typeparamref name="Protocol.ProtocolMessage"/> for exclude features e.g. -TCP4.
		/// </summary>
		/// <param name="protocolMessage">A <typeparamref name="Protocol.ProtocolMessage"/> to check for exclude features.</param>
		/// <returns>A string-collection of the exclude features in the <paramref name="protocolMessage"/>.</returns>
		internal static IEnumerable<string> GetExcludeFeaturesFromMessage(Protocol.ProtocolMessage protocolMessage)
		{
			return (from p in protocolMessage.Parameters
					where p.ParameterValue.StartsWith("-") && p.ParameterValue.Length > 1
					select p.ParameterValue.Substring(1));
		}

		/// <summary>
		/// Ensures the specified <paramref name="node"/> has all of the included, and none of the excluded features 
		/// in it's <see cref="Identity.ClientExtendedFeatures"/> list.
		/// </summary>
		/// <param name="node">The Node to check against.</param>
		/// <param name="includeFeatures">A string-collection of Client Features that MUST be present.</param>
		/// <param name="excludeFeatures">A string-collection of Client Features that MUST NOT be present.</param>
		/// <returns>Returns true if the <paramref name="node"/> satisfies the conditions of the include and exclude feature collections.</returns>
		internal static bool CheckFeaturesAgainstNode(RemoteMachine.Node node, IEnumerable<string> includeFeatures, IEnumerable<string> excludeFeatures)
		{
			if (node == null)
				return false;

			bool inclOkay = false;
			bool exclOkay = false;

			if (includeFeatures.Count() > 0)
			{
				IEnumerable<string> inclRes = node.Identity.ClientExtendedFeatures.Intersect(includeFeatures);
				if (includeFeatures.Count() == inclRes.Count())
					inclOkay = true;
			}
			else
				inclOkay = true;

			if (excludeFeatures.Count() > 0)
			{
				IEnumerable<string> exclRes = node.Identity.ClientExtendedFeatures.Intersect(excludeFeatures);
				if (exclRes.Count() == 0)
					exclOkay = true;
			}
			else
				exclOkay = true;

			return (inclOkay && exclOkay);
		}

		internal static void SendSearchResultToRequestingNode(RemoteMachine.Node sourceNode, RemoteMachine.Node targetNode, Protocol.ProtocolMessage protoMessage)
		{
			if (sourceNode == null || targetNode == null || protoMessage == null)
				return;

			if (protoMessage.MessageType == NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Direct ||
				protoMessage.MessageType == NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Echo)
			{
				if (targetNode != null)
				{
					string messageToNodes = protoMessage.ToString();
					targetNode.Connection.Send(messageToNodes);

					if (sourceNode != null)
						if (protoMessage.MessageType == NetfractionHub.Network.Protocol.ProtocolMessage.MessageTypeEnum.Echo)
							sourceNode.Connection.Send(messageToNodes);
				}
			}
		}

		/// <summary>
		/// Retrieves a Node by it's associated Session ID. The node can have any ConnectionState.
		/// </summary>
		/// <param name="sessionId">The 4-character, Base32-encoded Session ID of the requested node.</param>
		/// <returns>If the <paramref name="sessionId"/> is valid, the associated Node object is returned. Otherwise null.</returns>
		internal static RemoteMachine.Node GetNodeBySessionId(string sessionId)
		{
			if (string.IsNullOrEmpty(sessionId))
				return null;

			var node = _connectedNodes.AsReadOnly().Where(c => c.SessionId == sessionId);
			if (node.Count() > 0)
				return node.First();

			return null;
		}

		/// <summary>
		/// Retrieves a Node by it's associated Nickname. The node's ConnectionState must be Normal or Data.
		/// </summary>
		/// <param name="nickname">The Nickname of the node.</param>
		/// <returns>If the <paramref name="nickname"/> is valid, the associated Node object is returned. Otherwise null.</returns>
		internal static RemoteMachine.Node GetConnectedNodeByNickname(string nickname)
		{
			if (string.IsNullOrEmpty(nickname))
				return null;

			var node = from p in _connectedNodes.AsReadOnly()
						where p.Identity.Nickname.ToLower() == nickname.ToLower() && (p.Connection.State == NetfractionHub.RemoteMachine.ConnectionState.Normal || p.Connection.State == NetfractionHub.RemoteMachine.ConnectionState.Data)
						select p;
			if (node.Count() > 0)
				return node.First();

			return null;
		}

		/// <summary>
		/// Retrieves a Node by it's associated Client ID. The node's ConnectionState must be Normal or Data.
		/// </summary>
		/// <param name="clientId">The 39-character, Base32-encoded Client ID of the requested node.</param>
		/// <returns>If the <paramref name="clientId"/> is valid, the associated Node object is returned. Otherwise null.</returns>
		internal static RemoteMachine.Node GetConnectedNodeByClientId(string clientId)
		{
			if (string.IsNullOrEmpty(clientId))
				return null;

			var node = from p in _connectedNodes.AsReadOnly()
						where p.Identity.ClientId == clientId && (p.Connection.State == NetfractionHub.RemoteMachine.ConnectionState.Normal || p.Connection.State == NetfractionHub.RemoteMachine.ConnectionState.Data)
						select p;
			if (node.Count() > 0)
				return node.First();

			return null;
		}

		internal static void SendGenericStatusMessageToNode(NetfractionHub.RemoteMachine.Node targetNode, string message)
		{
			if (targetNode == null)
				return;

			SendStatusMessageToNode(targetNode, FactorySTA.StatusSeverity.Success, FactorySTA.StatusErrorCode.Generic, message);
		}

		internal static void SendStatusMessageToNode(NetfractionHub.RemoteMachine.Node targetNode, FactorySTA.StatusSeverity severity, FactorySTA.StatusErrorCode statusCode)
		{
			if (targetNode == null)
				return;

			SendStatusMessageToNode(targetNode, severity, statusCode, string.Empty);
		}

		internal static void SendStatusMessageToNode(NetfractionHub.RemoteMachine.Node targetNode, FactorySTA.StatusSeverity severity, FactorySTA.StatusErrorCode statusCode, string message)
		{
			if (targetNode == null)
				return;

			Protocol.ProtocolMessageFactory factory = new FactorySTA();
			Dictionary<int, object> factoryParams = new Dictionary<int, object>();
			factoryParams.Add((int)FactorySTA.ParameterEnum.Code, statusCode);
			factoryParams.Add((int)FactorySTA.ParameterEnum.Severity, severity);

			if (message != string.Empty)
				factoryParams.Add((int)FactorySTA.ParameterEnum.Description, message);

			targetNode.Connection.Send(factory.Create(Protocol.ProtocolMessage.MessageTypeEnum.Info, factoryParams).ToString());
		}
	}
}
